vue3+TS+Vite2+Element Plus管理系统通用模板

您所在的位置:网站首页 AutoFill Form 插件 vue3+TS+Vite2+Element Plus管理系统通用模板

vue3+TS+Vite2+Element Plus管理系统通用模板

2023-05-09 07:31| 来源: 网络整理| 查看: 265

搭建一个通用的后台管理系统框架,使用目前主流技术栈:vue3+TypeScript+Vite2+Pinia+Element Plus+VueRouter

初始化项目

安装vue-ts模板,和vite,并设置项目名称GBT(General Background Template)。

// npm npm init vite@latest GBT --template vue-ts // yarn yarn create vite GBT --template vue-ts 复制代码 启动项目:

初始化项目后安装项目依赖,之后执行npm run dev命令启动项目,浏览器打开 http://127.0.0.1:5173/ 就可以看到启动后的项目

npm install --registry=https://registry.npm.taobao.org //yarn install npm run dev 复制代码

image.png

安装Element-Plus

Element-Plus官方文档有两种引入方式,这里使用全局引入。

// 图标组件需要单独安装 npm install element-plus @element-plus/icons-vue sass --registry=https://registry.npm.taobao.org 复制代码

1.全局注册组件

在main.ts中引入ElementPlus和样式文件,并通过use方法安装ElementPlus插件。

// main.ts import ElementPlus from 'element-plus' import 'element-plus/theme-chalk/index.css' createApp(App) .use(ElementPlus) .mount('#app') 复制代码

2.全局组件类型声明&路径别名配置

在tsconfig.json文件中对ts进行配置,配置正确会有类型提示,这也是使用ts的好处。

// tsconfig.json { "compilerOptions": { // ... "types": ["element-plus/global"], "baseUrl": "./", // 解析非相对模块的基础地址,默认是当前目录 "paths": {"@/*": ["src/*"]}, // 路径映射,相对于baseUrl "allowSyntheticDefaultImports": true // 允许默认导入 } } 复制代码 配置环境变量

开发环境配置:.env.development

# 变量必须以 VITE_ 为前缀才能暴露给外部读取 VITE_APP_TITLE = 'MGT' ENV = 'development' VITE_APP_PORT = 3080 VITE_APP_BASE_API = '/dev-api' VITE_APP_BASE_API_MOCK = 'https://mock.mengxuegu.com/mock/636eff7ef22edd4bbbcd9919/mmServer' 复制代码

生产环境配置:.env.production

VITE_APP_TITLE = 'MGT' ENV = 'production' VITE_APP_BASE_API = 'https://mock.mengxuegu.com/prod' 复制代码

测试环境配置:.env.testing

VITE_APP_TITLE = 'MGT' ENV = 'testing' VITE_APP_BASE_API = 'https://mock.mengxuegu.com/testing' 复制代码 Vite配置

官方文档:Home | Vite中文网 (vitejs.cn) 先安装ts的类型描述文件,再在vite.config.ts文件中配置代理服务可以解决跨域问题。 在tsconfig.node.json文件中配置基础路径和路径映射。

// 安装TypeScript类型描述文件 npm install @types/node -S --registry=https://registry.npm.taobao.org // vite.config.ts import { UserConfig, ConfigEnv, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' export default ({ command, mode }: ConfigEnv): UserConfig => { const env = loadEnv(mode, process.cwd()) return { plugins: [ vue(),], server: { host: '0.0.0.0', port: Number(env.VITE_APP_PORT), open: true, proxy: { [env.VITE_APP_BASE_API]: { target: 'https://mock.mengxuegu.com/mock/636eff7ef22edd4bbbcd9919/mmServer', changeOrigin: true, rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '') } } }, resolve: { alias: { '@': path.resolve('./src') } } } } 复制代码 // tsconfig.node.json { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true, "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录 "paths": { "@/*": ["src/*"] } //路径映射,相对于baseUrl }, "include": ["vite.config.ts"] } 复制代码 自动导入插件

由于vue3的api是要先引入再使用的,这样每次都要引入就会很麻烦,于是相应的插件应运而生,一个是引入api的一个是引入组件的。

npm install unplugin-auto-import unplugin-vue-components -D --registry=https://registry.npm.taobao.org 复制代码 // vite.config.ts import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()], include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/], imports: [ // 插件预设支持导入的api 'vue' ], dts: './auto-imports.d.ts' }), Components({ resolvers: [ElementPlusResolver()] }) ], 复制代码

tsconfig.json文件中添加"./auto-imports.d.ts",否则识别不到会不生效。

// tsconfig.json "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","./auto-imports.d.ts"], 复制代码

启动项目测试一下,能否正常运行。

// App.vue omg 成功了 复制代码 Pinia状态管理

新一代状态管理工具,好用!

1.pinia安装

npm install pinia --registry=https://registry.npm.taobao.org 复制代码

2.pinia注册

// src/main.ts import { createPinia } from "pinia" createApp(App) .use(ElementPlus) .use(createPinia()) .mount('#app') 复制代码

3.pinia模块封装

// store/modules/user.ts import { defineStore } from 'pinia' import { UserState } from '../storeTypes' const useUserStore = defineStore({ id: 'user', state: (): UserState => ({ token: '', roles: [], perms: [] }), actions: {} }) export default useUserStore 复制代码 // src\store\storeTypes.ts export interface UserState { token: string nickname?: string avatar?: string roles: string[] perms: string[] } 复制代码 // store/index.ts import useUserStore from './modules/user'; const useStore = () => ({ user: useUserStore(), }); export default useStore; 复制代码 Axios封装

1.安装axios

npm install axios --registry=https://registry.npm.taobao.org 复制代码

2.浏览器缓存封装

// utils/storage.ts // window.localStorage 浏览器永久缓存 export const localStorage = { // 设置永久缓存 set(key: string, val: any) { window.localStorage.setItem(key, JSON.stringify(val)); }, // 获取永久缓存 get(key: string) { const json: any = window.localStorage.getItem(key); return JSON.parse(json); }, // 移除永久缓存 remove(key: string) { window.localStorage.removeItem(key); }, // 移除全部永久缓存 clear() { window.localStorage.clear(); } }; // window.sessionStorage 浏览器临时缓存 export const sessionStorage = { // 设置临时缓存 set(key: string, val: any) { window.sessionStorage.setItem(key, JSON.stringify(val)); }, // 获取临时缓存 get(key: string) { const json: any = window.sessionStorage.getItem(key); return JSON.parse(json); }, // 移除临时缓存 remove(key: string) { window.sessionStorage.removeItem(key); }, // 移除全部临时缓存 clear() { window.sessionStorage.clear(); } }; 复制代码

3.请求封装

实际开发过程中接口不会很快就出来,所以mock接口是必要的,那么在封装的时候就可以考虑mock接口的功能加进去。

// src\utils\request.ts import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' import { localStorage } from '../utils/storage' import useStore from '../store' import { ElMessage, ElMessageBox } from 'element-plus' const baseURLAll = ref(importa.env.VITE_APP_BASE_API) type TOptions = { mock?: boolean // 是否启用mock mockUrl?: string // 自定义mock地址 headersContentType?: string cusFileName?: string } interface YXRequestConfig extends AxiosRequestConfig { headers: any } const request = (options: TOptions & AxiosRequestConfig) => { if (options?.mock) { baseURLAll.value = options.mockUrl ?? importa.env.VITE_APP_BASE_API_MOCK } else { baseURLAll.value = importa.env.VITE_APP_BASE_API } const service = axios.create({ baseURL: baseURLAll.value, timeout: 50000, headers: { 'Content-Type': options?.headersContentType ?? 'application/json' } }) // 请求拦截器 service.interceptors.request.use( (config: YXRequestConfig) => { if (!config.headers) throw new Error(`Expected 'config' and 'config.headers' not to be undefined`) const { user } = useStore() if (user.token) config.headers['Authorization'] = `${localStorage.get('token')}` return config }, (error) => { return Promise.reject(error) } ) // 响应拦截器 function download(content: any, fileName: any) { const blob = new Blob([content]) //创建一个类文件对象:Blob对象表示一个不可变的、原始数据的类文件对象 const url = window.URL.createObjectURL(blob) //URL.createObjectURL(object)表示生成一个File对象或Blob对象 let dom = document.createElement('a') //设置一个隐藏的a标签,href为输出流,设置download dom.style.display = 'none' dom.href = url dom.setAttribute('download', fileName) //指示浏览器下载url,而不是导航到它;因此将提示用户将其保存为本地文件 document.body.appendChild(dom) dom.click() } service.interceptors.response.use( (response: AxiosResponse) => { // 文件下载 if (response.config.responseType === 'blob') { const content = response.data //返回的内容 const fileName = options?.cusFileName ?? '文件.xls' //下载文件名 download(content, fileName) return } const { code, msg } = response.data if (code === 200) { return response.data } else { ElMessage({ message: msg || '系统出错', type: 'error' }) return Promise.reject(new Error(msg || 'Error')) } }, (error) => { const { msg, code } = error.response.data if (code === 'A0230') { // token 过期 localStorage.clear() // 清除浏览器全部缓存 window.location.href = '/' // 跳转登录页 ElMessageBox.alert('当前页面已失效,请重新登录', '提示', {}) .then(() => {}) .catch(() => {}) } else { ElMessage({ message: msg || '系统出错', type: 'error' }) } return Promise.reject(new Error(msg || 'Error')) } ) return service(options) } export default request 复制代码

4.API封装

// src\api\user\index.ts import request from '@/utils/request' import { AxiosPromise } from 'axios' import { UserInfo } from './types' export function getUserInfo(data: { userId: Number | null }): AxiosPromise { return request({ url: '/api/users/userInfo', method: 'post', data }) } 复制代码 // src\api\user\types.ts export interface UserInfo { nickname: string; avatar: string; roles: string[]; perms: string[]; } 复制代码 utils // src\utils\index.ts /** * Check if an element has a class * @param {HTMLElement} elm * @param {string} cls * @returns {boolean} */ export function hasClass(ele: HTMLElement, cls: string) { return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')); } /** * Add class to element * @param {HTMLElement} elm * @param {string} cls */ export function addClass(ele: HTMLElement, cls: string) { if (!hasClass(ele, cls)) ele.className += ' ' + cls; } /** * Remove class from element * @param {HTMLElement} elm * @param {string} cls */ export function removeClass(ele: HTMLElement, cls: string) { if (hasClass(ele, cls)) { const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)'); ele.className = ele.className.replace(reg, ' '); } } export function mix(color1: string, color2: string, weight: number) { weight = Math.max(Math.min(Number(weight), 1), 0); const r1 = parseInt(color1.substring(1, 3), 16); const g1 = parseInt(color1.substring(3, 5), 16); const b1 = parseInt(color1.substring(5, 7), 16); const r2 = parseInt(color2.substring(1, 3), 16); const g2 = parseInt(color2.substring(3, 5), 16); const b2 = parseInt(color2.substring(5, 7), 16); const r = Math.round(r1 * (1 - weight) + r2 * weight); const g = Math.round(g1 * (1 - weight) + g2 * weight); const b = Math.round(b1 * (1 - weight) + b2 * weight); const rStr = ('0' + (r || 0).toString(16)).slice(-2); const gStr = ('0' + (g || 0).toString(16)).slice(-2); const bStr = ('0' + (b || 0).toString(16)).slice(-2); return '#' + rStr + gStr + bStr; } 复制代码 动态权限路由&router

1. 安装 vue-router

npm install vue-router@next nprogress @types/nprogress --registry=https://registry.npm.taobao.org 复制代码

2. 创建路由实例

创建路由实例并导出,其中包括静态路由数据,动态路由后面将通过接口从后端获取并整合用户角色的权限控制。

// 新建src\layout\index.vue文件 // 新建src\views\error-page\401.vue文件 // 新建src\views\error-page\404.vue文件 // 新建src\views\login\index.vue文件 // 新建src\views\redirect\index.vue文件 // 新建src\views\dashboard\index.vue文件 // src/router/index.ts import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' import useStore from '@/store' export const Layout = () => import('@/layout/index.vue') // 静态路由 export const constantRoutes: Array = [ { path: '/redirect', component: Layout, meta: { hidden: true }, children: [ { path: '/redirect/:path(.*)', component: () => import('@/views/redirect/index.vue') } ] }, { path: '/login', component: () => import('@/views/login/index.vue'), meta: { hidden: true } }, { path: '/404', component: () => import('@/views/error-page/404.vue'), meta: { hidden: true } }, { path: '/401', component: () => import('@/views/error-page/401.vue'), meta: { hidden: true } }, { path: '/', component: Layout, redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('@/views/dashboard/index.vue'), name: 'Dashboard', meta: { title: 'dashboard', icon: 'dashboard', affix: true } } ] } ] // 创建路由实例 const router = createRouter({ history: createWebHashHistory(), routes: constantRoutes as RouteRecordRaw[], // 刷新时,滚动条位置还原 scrollBehavior: () => ({ left: 0, top: 0 }) }) // 重置路由 export function resetRouter() { // doing... } export default router 复制代码

3. 路由实例全局注册

// main.ts import router from "@/router"; // import '@/permission'; createApp(App).use(router).use(ElementPlus).use(createPinia()).mount('#app') 复制代码 // App.vue const isRouterAlive = ref(true) const reload = () => { isRouterAlive.value = false nextTick(() => (isRouterAlive.value = true)) } provide('reload', reload) 复制代码

试一试!启动项目!成功一大半!

4.登录退出等api封装

auth

// src\api\auth\types.ts /** * 登录表单类型声明 */ export interface LoginForm { username: string password: string grant_type: string } 复制代码 // src\api\auth\index.ts import request from '@/utils/request' import { AxiosPromise } from 'axios' import { LoginForm, VerifyCode } from './types' /** * * @param data {LoginForm} * @returns */ export function loginApi(data: LoginForm): AxiosPromise { return request({ url: '/api/auth/login', method: 'post', params: data, }) } /** * 注销 */ export function logoutApi() { return request({ url: '/api/auth/logout', method: 'delete' }) } 复制代码 // src\api\menu\types.ts /** * 菜单查询参数类型声明 */ export interface MenuQuery { keywords?: string } /** * 菜单分页列表项声明 */ export interface Menu { id?: number parentId: number type?: string | 'CATEGORY' | 'MENU' | 'EXTLINK' createTime: string updateTime: string name: string icon: string component: string sort: number visible: number children: Menu[] } /** * 菜单表单类型声明 */ export interface MenuForm { //菜单ID id?: string //父菜单ID parentId: string //菜单名称 name: string //菜单是否可见(1:是;0:否;) visible: number icon?: string //排序 sort: number //组件路径 component?: string //路由路径 path: string //跳转路由路径 redirect?: string //菜单类型 type: string //权限标识 perm?: string } /** * 资源(菜单+权限)类型 */ export interface Resource { // 菜单值 value: string //菜单文本 label: string //子菜单 children: Resource[] } /** * 权限类型 */ export interface Permission { // 权限值 value: string //权限文本 label: string } 复制代码 // src\api\menu\index.ts import request from '@/utils/request' import { AxiosPromise } from 'axios' import { MenuQuery, Menu, Resource, MenuForm } from './types' type OptionType = { value: string label: string checked?: boolean children?: OptionType[] } /** * 获取路由列表 */ export function listRoutes() { return request({ url: '/api/v1/menus/routes', method: 'get' }) } /** * 获取菜单表格列表 * * @param queryParams */ export function listMenus(queryParams: MenuQuery): AxiosPromise { return request({ url: '/api/v1/menus', method: 'get', params: queryParams }) } /** * 获取菜单下拉树形列表 */ export function listMenuOptions(): AxiosPromise { return request({ url: '/api/v1/menus/options', method: 'get' }) } /** * 获取资源(菜单+权限)树形列表 */ export function listResources(): AxiosPromise { return request({ url: '/api/v1/menus/resources', method: 'get' }) } /** * 获取菜单详情 * @param id */ export function getMenuDetail(id: string): AxiosPromise { return request({ url: '/api/v1/menus/' + id, method: 'get' }) } /** * 添加菜单 * * @param data */ export function addMenu(data: MenuForm) { return request({ url: '/api/v1/menus', method: 'post', data: data }) } /** * 修改菜单 * * @param id * @param data */ export function updateMenu(id: string, data: MenuForm) { return request({ url: '/api/v1/menus/' + id, method: 'put', data: data }) } /** * 批量删除菜单 * * @param ids 菜单ID,多个以英文逗号(,)分割 */ export function deleteMenus(ids: string) { return request({ url: '/api/v1/menus/' + ids, method: 'delete' }) } 复制代码

5.store公共方法

// src\store\storeTypes.ts import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'; export interface AppState { device: string; sidebar: { opened: boolean; withoutAnimation: boolean; }; language?: string; size: string; } export interface PermissionState { routes: RouteRecordRaw[]; addRoutes: RouteRecordRaw[]; } export interface SettingState { theme: string; tagsView: boolean; fixedHeader: boolean; showSettings: boolean; sidebarLogo: boolean; } export interface UserState { token: string; nickname: string; avatar: string; roleList?: string[]; perms: string[]; roles: string[]; userId: Number | null; } export interface TagView extends Partial { title?: string; } export interface TagsViewState { visitedViews: TagView[]; cachedViews: string[]; } 复制代码 // src/store/modules/user.ts import { defineStore } from 'pinia' import { UserState } from '../storeTypes' import { localStorage } from '@/utils/storage' import { loginApi, logoutApi } from '@/api/auth' import { getUserInfo } from '@/api/user' import { resetRouter } from '@/router' import { LoginForm } from '@/api/auth/types' const useUserStore = defineStore({ id: 'user', state: (): UserState => ({ token: localStorage.get('token') || '', nickname: '', avatar: '', userId: null, roles: [], perms: [] }), actions: { // 重置仓库到初始状态 async RESET_STATE() { this.$reset() }, // 登录 login(data: LoginForm) { const { username, password } = data return new Promise((resolve, reject) => { loginApi({ grant_type: 'password', username: username.trim(), password: password }) .then((response: { data: any }) => { const { userId, token } = response.data localStorage.set('token', token) this.token = token this.userId = userId resolve(token) }) .catch((error) => { reject(error) }) }) }, // 获取用户信息(昵称、头像、角色集合、权限集合) getUserInfo() { return new Promise((resolve, reject) => { getUserInfo({ userId: this.userId }) .then(({ data }) => { if (!data) { return reject('Verification failed, please Login again.') } const { nickname, avatar, roles, perms } = data if (!roles || roles.length { reject(error) }) }) }, // 注销 logout() { return new Promise((resolve, reject) => { logoutApi() .then(() => { localStorage.remove('token') this.RESET_STATE() resetRouter() resolve(null) }) .catch((error) => { reject(error) }) }) }, // 清除 Token resetToken() { return new Promise((resolve) => { localStorage.remove('token') this.RESET_STATE() resolve(null) }) } } }) export default useUserStore 复制代码 // src\store\modules\permission.ts import { PermissionState } from '../storeTypes' import { RouteRecordRaw } from 'vue-router' import { defineStore } from 'pinia' import { constantRoutes } from '@/router' import { listRoutes } from '@/api/menu' const modules = importa.glob('../../views/**/**.vue') export const Layout = () => import('@/layout/index.vue') const hasPermission = (roles: string[], route: RouteRecordRaw) => { if (route.meta && route.meta.roles) { if (roles.includes('ROOT')) { return true } return roles.some((role) => { if (route.meta?.roles !== undefined) { return (route.meta.roles as string[]).includes(role) } }) } return false } // 角色过滤路由 export const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => { const res: RouteRecordRaw[] = [] routes.forEach((route) => { const tmp = { ...route } as any if (hasPermission(roles, tmp)) { if (tmp.component == 'Layout') { tmp.component = Layout } else { const component = modules[`../../views/${tmp.component}.vue`] as any if (component) { tmp.component = component } else { tmp.component = modules[`../../views/error-page/404.vue`] } } res.push(tmp) if (tmp.children) { tmp.children = filterAsyncRoutes(tmp.children, roles) } } }) return res } const usePermissionStore = defineStore({ id: 'permission', state: (): PermissionState => ({ routes: [], // 静态路由 + 动态路由 addRoutes: [] //动态路由 }), actions: { setRoutes(routes: RouteRecordRaw[]) { this.addRoutes = routes this.routes = constantRoutes.concat(routes) }, generateRoutes(roles: string[]) { return new Promise((resolve, reject) => { listRoutes() .then((response) => { const asyncRoutes = response.data const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) this.setRoutes(accessedRoutes) resolve(accessedRoutes) }) .catch((error) => { reject(error) }) }) } } }) export default usePermissionStore 复制代码 // src\store\index.ts import useUserStore from './modules/user' import usePermissionStore from './modules/permission' const useStore = () => ({ user: useUserStore(), permission: usePermissionStore(), }) export default useStore 复制代码

6. 动态权限路由,路由鉴权,鉴权文件引入

// main.ts引入 // src/permission.ts import router from '@/router' import { ElMessage } from 'element-plus' import useStore from '@/store' import NProgress from 'nprogress' import 'nprogress/nprogress.css' NProgress.configure({ showSpinner: false }) // 进度环显示/隐藏 // 白名单路由 const whiteList = ['/login'] router.beforeEach(async (to, form, next) => { NProgress.start() const { user, permission } = useStore() const hasToken = user.token // 有token if (hasToken) { // 登录成功,跳转到首页 if (to.path === '/login') { next({ path: '/' }) NProgress.done() } else { const hasGetUserInfo = user.roles.length > 0 // 有用户信息 if (hasGetUserInfo) { next() } else { // 无用户信息,重新获取用户信息路由信息 try { await user.getUserInfo() const roles = user.roles // 用户拥有权限的路由集合(accessRoutes) // 是根据用户角色获取拥有权限的路由(静态路由+动态路由) const accessRoutes: any = await permission.generateRoutes(roles) accessRoutes.forEach((route: any) => { router.addRoute(route) }) next({ ...to, replace: true }) } catch (error) { // 移除 token 并跳转登录页 await user.resetToken() ElMessage.error((error as any) || 'Has Error') next(`/login?redirect=${to.path}`) NProgress.done() } } } } else { // 未登录可以访问白名单页面(登录页面),无token if (whiteList.indexOf(to.path) !== -1) { next() } else { next(`/login?redirect=${to.path}`) NProgress.done() } } }) router.afterEach(() => { NProgress.done() }) 复制代码

启动项目测试权限控制

SVG图标

1. 安装 vite-plugin-svg-icons

npm i [email protected] [email protected] -D --registry=https://registry.npm.taobao.org 复制代码

2. 创建图标文件夹

​ 项目创建 src/assets/icons 文件夹,存放 iconfont 下载的 SVG 图标

3. main.ts 引入注册脚本

// main.ts import 'virtual:svg-icons-register'; 复制代码

4. vite.config.ts 插件配置

// vite.config.ts import {UserConfig, ConfigEnv, loadEnv} from 'vite' import vue from '@vitejs/plugin-vue' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; export default ({command, mode}: ConfigEnv): UserConfig => { // 获取 .env 环境配置文件 const env = loadEnv(mode, process.cwd()) return ( { plugins: [ //... createSvgIconsPlugin({ // 指定需要缓存的图标文件夹 iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')], // 指定symbolId格式 symbolId: 'icon-[dir]-[name]', }) ] } ) } 复制代码

5. TypeScript支持

// tsconfig.json { "compilerOptions": { "types": ["vite-plugin-svg-icons/client"] } } 复制代码

6. 组件封装

import { computed } from 'vue'; const props=defineProps({ prefix: { type: String, default: 'icon', }, iconClass: { type: String, required: true, }, color: { type: String, default: '' } }) const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`); .svg-icon { width: 1em; height: 1em; vertical-align: -0.15em; overflow: hidden; fill: currentColor; } 复制代码

7. 使用示例

import SvgIcon from '@/components/SvgIcon/index.vue'; 复制代码

样式文件

// src\styles\index.scss body { margin: 0; padding: 0; height: 100%; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; } label { font-weight: 700; } html { height: 100%; box-sizing: border-box; } #app { height: 100%; } *, *:before, *:after { box-sizing: inherit; } a:focus, a:active { outline: none; } a, a:focus, a:hover { cursor: pointer; color: inherit; text-decoration: none; } div:focus { outline: none; } .clearfix { &:after { visibility: hidden; display: block; font-size: 0; content: ' '; clear: both; height: 0; } } // main-container global css .app-container { padding: 20px; } .search{ padding:18px 0 0 10px; margin-bottom: 10px; box-shadow: var(--el-box-shadow-light); border-radius: var(--el-card-border-radius); border: 1px solid var(--el-card-border-color); } 复制代码 登录页面 //src\views\login\index.vue 登录 login username: admin password: 123456 import router from '@/router' import SvgIcon from '@/components/SvgIcon/index.vue' import useStore from '@/store' import { useRoute } from 'vue-router' import { LoginForm } from '@/api/auth/types' const { user } = useStore() const route = useRoute() const loginFormRef = ref(ElForm) const passwordRef = ref(ElInput) const state = reactive({ redirect: '', loginForm: { username: 'admin', password: '123456' } as LoginForm, loginRules: { username: [{ required: true, trigger: 'blur' }], password: [{ required: true, trigger: 'blur', validator: validatePassword }] }, loading: false, passwordType: 'password', // 大写提示禁用 capslockTooltipDisabled: true, otherQuery: {}, clientHeight: document.documentElement.clientHeight, showDialog: false }) function validatePassword(rule: any, value: any, callback: any) { if (value.length < 6) { callback(new Error('The password can not be less than 6 digits')) } else { callback() } } const { loginForm, loginRules, loading, passwordType, capslockTooltipDisabled } = toRefs(state) function checkCapslock(e: any) { const { key } = e state.capslockTooltipDisabled = key && key.length === 1 && key >= 'A' && key { passwordRef.value.focus() }) } //登录 function handleLogin() { loginFormRef.value.validate((valid: boolean) => { if (valid) { state.loading = true user .login(state.loginForm) .then(() => { router.push({ path: state.redirect || '/', query: state.otherQuery }) state.loading = false }) .catch(() => { state.loading = false }) } else { return false } }) } watch( route, () => { const query = route.query if (query) { state.redirect = query.redirect as string state.otherQuery = getOtherQuery(query) } }, { immediate: true } ) function getOtherQuery(query: any) { return Object.keys(query).reduce((acc: any, cur: any) => { if (cur !== 'redirect') { acc[cur] = query[cur] } return acc }, {}) } $bg: #a7c3e6; $light_gray: #fff; $cursor: #fff; /* reset element-ui css */ .login-container { .title-container { position: relative; .title { font-size: 26px; color: $light_gray; margin: 0px auto 40px auto; text-align: center; font-weight: bold; } .set-language { color: #fff; position: absolute; top: 3px; font-size: 18px; right: 0px; cursor: pointer; } } .el-input { display: inline-block; height: 36px; width: 85%; .el-input__wrapper { padding: 0; background: transparent; box-shadow: none; width: 100%; .el-input__inner { background: transparent; border: 0px; -webkit-appearance: none; border-radius: 0px; color: $light_gray; height: 36px; caret-color: $cursor; &:-webkit-autofill { box-shadow: 0 0 0px 1000px $bg inset !important; -webkit-text-fill-color: $cursor !important; } } } } .el-input__inner { &:hover { border-color: var(--el-input-hover-border, var(--el-border-color-hover)); box-shadow: none; } box-shadow: none; } .el-form-item { border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.1); border-radius: 5px; color: #454545; } .copyright { width: 100%; position: absolute; bottom: 0; font-size: 12px; text-align: center; color: #cccccc; } } $bg: #2d3a4b; $dark_gray: #889aa4; $light_gray: #eee; .login-container { min-height: 100%; width: 100%; background-color: $bg; overflow: hidden; .login-form { position: relative; width: 520px; max-width: 100%; padding: 160px 35px 0; margin: 0 auto; overflow: hidden; } .tips { font-size: 14px; color: #fff; margin-bottom: 10px; span { &:first-of-type { margin-right: 16px; } } } .svg-container { padding: 5px 10px; color: $dark_gray; vertical-align: middle; width: 30px; display: inline-block; } .title-container { position: relative; .title { font-size: 26px; color: $light_gray; margin: 0px auto 40px auto; text-align: center; font-weight: bold; } } .show-pwd { position: absolute; right: 10px; top: 7px; font-size: 16px; color: $dark_gray; cursor: pointer; user-select: none; } .captcha { position: absolute; right: 0; top: 0; img { height: 42px; cursor: pointer; vertical-align: middle; } } } .thirdparty-button { position: absolute; right: 40px; bottom: 6px; } @media only screen and (max-width: 470px) { .thirdparty-button { display: none; } } 复制代码

!!!到这里一个管理系统最基础的部分就算完成了,如果项目创新程度比较高可以从这里开始:refreshing分支。 如果要做一个比较传统的管理后台可以选择master分支!!!

layout布局组件

传统基础布局

image.png

按钮权限

1. Directive 自定义指令

// src/directive/permission/index.ts import useStore from "@/store"; import { Directive, DirectiveBinding } from "vue"; /** * 按钮权限校验 */ export const hasPerm: Directive = { mounted(el: HTMLElement, binding: DirectiveBinding) { // 「超级管理员」拥有所有的按钮权限 const { user } = useStore() const roles = user.roles; if (roles.includes('ROOT')) { return true } // 「其他角色」按钮权限校验 const { value } = binding; if (value) { const requiredPerms = value; // DOM绑定需要的按钮权限标识 const hasPerm = user.perms.some(perm => { return requiredPerms.includes(perm) }) if (!hasPerm) { el.parentNode && el.parentNode.removeChild(el); } } else { throw new Error("need perms! Like v-has-perm="['sys:user:add','sys:user:edit']""); } } }; 复制代码

2. 自定义指令全局注册

// src/main.ts const app = createApp(App) // 自定义指令 import * as directive from "@/directive"; Object.keys(directive).forEach(key => { app.directive(key, (directive as { [key: string]: Directive })[key]); }); 复制代码

3. 指令使用

// src/views/system/user/index.vue 新增 删除 复制代码


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3